/* Copyright (C) 1999-2005 by Peter Eastman

   This program is free software; you can redistribute it and/or modify it under the
   terms of the GNU General Public License as published by the Free Software
   Foundation; either version 2 of the License, or (at your option) any later version.

   This program is distributed in the hope that it will be useful, but WITHOUT ANY 
   WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A 
   PARTICULAR PURPOSE.  See the GNU General Public License for more details. */

package artofillusion;

import artofillusion.image.*;
import artofillusion.material.*;
import artofillusion.math.*;
import artofillusion.object.*;
import artofillusion.texture.*;
import artofillusion.ui.*;
import buoy.event.*;
import buoy.widget.*;
import java.awt.*;
import java.awt.event.*;

/** MaterialPreviewer is a component used for renderering previews of Materials.  It displays
    a scene consisting of a Sphere with the desired Material applied to it, a ground plane,
    and a single light.  Optionally, an Object3D may be specified which will then be used
    instead of a Sphere. */

public class MaterialPreviewer extends CustomWidget implements RenderListener
{
  Scene theScene;
  Camera theCamera;
  ObjectInfo info;
  CoordinateSystem objectCoords;
  Image theImage;
  boolean mouseInside, renderInProgress;
  Point clickPoint;
  Mat4 rotMatrix;
  private BPopupMenu popup;
  private BMenuItem viewItem[];
  private BCheckBoxMenuItem objectItem[];
  private Object3D shape[];

  public static final int HANDLE_SIZE = 5;
  static final double DRAG_SCALE = Math.PI/360.0;

  /** Create a MaterialPreviewer to display a Texture and/or Material mapped to a sphere.
      Either tex or mat may be null. */

  public MaterialPreviewer(Texture tex, Material mat, int width, int height)
  {
    shape = new Object3D [] {
        new Sphere(1.0, 1.0, 1.0),
        new Cube(2.0, 2.0, 2.0),
        new Cylinder(2.0, 1.0, 1.0, 1.0),
        new Cylinder(2.0, 1.0, 1.0, 0.0)
      };
    ObjectInfo objInfo = new ObjectInfo(shape[0], new CoordinateSystem(), "");
    initObject(tex, mat, objInfo);
    init(objInfo, width, height);
    
    // Add menu items to the popup menu for changing the object.
    
    popup.addSeparator();
    objectItem = new BCheckBoxMenuItem [] {
      Translate.checkboxMenuItem("sphere", this, "changeObject", true),
      Translate.checkboxMenuItem("cube", this, "changeObject", false),
      Translate.checkboxMenuItem("cylinder", this, "changeObject", false),
      Translate.checkboxMenuItem("cone", this, "changeObject", false)
    };
    for (int i = 0; i < objectItem.length; i++)
      popup.add(objectItem[i]);
  }
  
  /** Same as above, except you can specify a different object to use instead of a sphere. */

  public MaterialPreviewer(Texture tex, Material mat, Object3D obj, int width, int height)
  {
    ObjectInfo objInfo = new ObjectInfo(obj, new CoordinateSystem(), "");
    initObject(tex, mat, objInfo);
    init(objInfo, width, height);
  }

  /** Create a MaterialPreviewer to display the specified object, with its current texture
      and material. */

  public MaterialPreviewer(ObjectInfo obj, int width, int height)
  {
    init(obj.duplicate(), width, height);
  }
  
  /** Initialize the object's texture and material. */
  
  private void initObject(Texture tex, Material mat, ObjectInfo objInfo)
  {
    if (tex == null)
      tex = UniformTexture.invisibleTexture();
    objInfo.setTexture(tex, tex.getDefaultMapping());
    if (mat != null)
      objInfo.setMaterial(mat, mat.getDefaultMapping());
  }
  
  /** Initialize the MaterialPreviewer. */
  
  private void init(ObjectInfo obj, int width, int height)
  {
    BoundingBox bounds = obj.getBounds();
    Vec3 size = bounds.getSize();
    double max = Math.max(size.x, Math.max(size.y, size.z))/2.0;
    double floor = -bounds.getSize().length()/2.0;
    CoordinateSystem coords = new CoordinateSystem(new Vec3(0.0, 0.0, 10.0*max), new Vec3(0.0, 0.0, -1.0), Vec3.vy());
    if (max > 10.0)
      max = 10.0;
    Vec3 vert[] = new Vec3 [] {new Vec3(100.0*max, floor, 100.0*max), new Vec3(-100.0*max, floor, 100.0*max), new Vec3(0.0, floor, -100.0*max)};
    int face[][] = {{0, 1, 2}};
    TriangleMesh tri;

    theScene = new Scene();
    theCamera = new Camera();
    theCamera.setCameraCoordinates(coords);
    coords = new CoordinateSystem(new Vec3(), new Vec3(-0.5, -0.4, -1.0), Vec3.vy());
    theScene.addObject(new DirectionalLight(new RGBColor(1.0f, 1.0f, 1.0f), 0.8f), coords, "", null);
    coords = new CoordinateSystem(new Vec3(), Vec3.vz(), Vec3.vy());
    theScene.addObject(tri = new TriangleMesh(vert, face), coords, "", null);
    Texture tex = theScene.getDefaultTexture();
    tri.setTexture(tex, tex.getDefaultMapping());
    info = obj;
    objectCoords = info.coords = new CoordinateSystem();
    theScene.addObject(info, null);
    setPreferredSize(new Dimension(width, height));
    addEventLink(MousePressedEvent.class, this, "mousePressed");
    addEventLink(MouseReleasedEvent.class, this, "mouseReleased");
    addEventLink(MouseClickedEvent.class, this, "mouseClicked");
    addEventLink(MouseEnteredEvent.class, this, "mouseEntered");
    addEventLink(MouseExitedEvent.class, this, "mouseExited");
    addEventLink(MouseDraggedEvent.class, this, "mouseDragged");
    addEventLink(RepaintEvent.class, this, "paint");
    
    // Build the popup menu.
    
    popup = new BPopupMenu();
    viewItem = new BMenuItem [] {
      new BMenuItem(Translate.text("Front")),
      new BMenuItem(Translate.text("Back")),
      new BMenuItem(Translate.text("Left")),
      new BMenuItem(Translate.text("Right")),
      new BMenuItem(Translate.text("Top")),
      new BMenuItem(Translate.text("Bottom"))
    };
    for (int i = 0; i < viewItem.length; i++)
    {
      popup.add(viewItem[i]);
      viewItem[i].addEventLink(CommandEvent.class, this, "changeView");
    }
    
    // Set up other listeners.
    
    getComponent().addComponentListener(new ComponentAdapter() {
      public void componentResized(ComponentEvent ev)
      {
        render();
      }
    });
    getComponent().addHierarchyListener(new HierarchyListener() {
      public void hierarchyChanged(HierarchyEvent ev)
      {
        if ((ev.getChangeFlags()&HierarchyEvent.DISPLAYABILITY_CHANGED) != 0)
          if (!getComponent().isDisplayable())
          {
            Renderer rend = ModellingApp.getPreferences().getTexturePreviewRenderer();
            if (rend != null)
              rend.cancelRendering(theScene);
          }
      }
    });
    render();
  }
  
  /** Get the object on which the texture and material are being displayed. */
  
  public ObjectInfo getObject()
  {
    return info;
  }
  
  /* The following methods are used to modify the properties of the object being displayed. */

  public void setTexture(Texture tex, TextureMapping map)
  {
    if (tex == null)
      tex = UniformTexture.invisibleTexture();
    if (map == null)
      map = tex.getDefaultMapping();
    info.setTexture(tex, map);
  }
  
  public void setMaterial(Material mat, MaterialMapping map)
  {
    info.setMaterial(mat, map);
  }

  /** Render the preview. */

  public synchronized void render()
  {
    Renderer rend = ModellingApp.getPreferences().getTexturePreviewRenderer();
    if (rend == null)
      return;
    rend.cancelRendering(theScene);
    Rectangle bounds = getBounds();
    if (bounds.width == 0 || bounds.height == 0)
      return;
    theCamera.setSize(bounds.width, bounds.height);
    theCamera.setDistToScreen((bounds.height/200.0)/Math.tan(0.14));
    rend.configurePreview();
    rend.renderScene(theScene, theCamera, this, null);
    renderInProgress = true;
    repaint();
  }

  /** Cancel rendering. */
  
  public synchronized void cancelRendering()
  {
    Renderer rend = ModellingApp.getPreferences().getTexturePreviewRenderer();
    if (rend != null)
      rend.cancelRendering(theScene);
  }

  private void paint(RepaintEvent ev)
  {
    Graphics2D g = ev.getGraphics();
    if (theImage != null)
      g.drawImage(theImage, 0, 0, getComponent());
    if (mouseInside)
      drawHilight(g);
    if (renderInProgress)
      {
        Rectangle bounds = getBounds();
        g.setColor(Color.red);
        g.drawRect(0, 0, bounds.width-1, bounds.height-1);
      }
  }

  private void drawHilight(Graphics g)
  {
    Rectangle bounds = getBounds();
    g.setColor(Color.red);
    g.fillRect(0, 0, HANDLE_SIZE, HANDLE_SIZE);
    g.fillRect(bounds.width-HANDLE_SIZE, 0, HANDLE_SIZE, HANDLE_SIZE);
    g.fillRect(0, bounds.height-HANDLE_SIZE, HANDLE_SIZE, HANDLE_SIZE);
    g.fillRect(bounds.width-HANDLE_SIZE, bounds.height-HANDLE_SIZE, HANDLE_SIZE, HANDLE_SIZE);
  }

  private void drawObject(Graphics g)
  {
    g.setColor(Color.gray);
    theCamera.setObjectTransform(rotMatrix.times(objectCoords.fromLocal()));
    Object3D.draw(g, theCamera, info.object.getWireframeMesh(), info.object.getBounds());
  }
  
  /** Rotate the object to show a specific side. */
  
  private void changeView(CommandEvent ev)
  {
    double angles[][] = new double [][] {
        {0.0, 0.0, 0.0}, 
        {0.0, 180.0, 0.0}, 
        {0.0, -90.0, 0.0}, 
        {0.0, 90.0, 0.0}, 
        {-90.0, 0.0, 0.0}, 
        {90.0, 0.0, 0.0}, 
    };
    for (int i = 0; i < viewItem.length; i++)
      if (viewItem[i] == ev.getWidget())
      {
        objectCoords.setOrientation(angles[i][0], angles[i][1], angles[i][2]);
        render();
        return;
      }
  }
  
  /** Change what object the texture/material is displayed ob. */
  
  private void changeObject(CommandEvent ev)
  {
    for (int i = 0; i < objectItem.length; i++)
    {
      objectItem[i].setState(ev.getWidget() == objectItem[i]);
      if (ev.getWidget() == objectItem[i])
      {
        shape[i].setTexture(info.object.getTexture(), info.object.getTextureMapping());
        shape[i].setMaterial(info.object.getMaterial(), info.object.getMaterialMapping());
        info.object = shape[i];
        render();
      }
    }
  }

  /** Called when more pixels are available for the current image. */
  
  public void imageUpdated(Image image)
  {
    theImage = image;
    repaint();
  }

  /** The renderer may call this method periodically during rendering, to give the listener text descriptions
      of the current status of rendering. */
  
  public void statusChanged(String status)
  {
  }

  /** Called when rendering is complete. */
  
  public void imageComplete(ComplexImage image)
  {
    theImage = image.getImage();
    renderInProgress = false;
    repaint();
  }
  
  /** Called when rendering is cancelled. */
  
  public void renderingCanceled()
  {
  }

  private void mouseEntered(MouseEnteredEvent e)
  {
    mouseInside = true;
    Graphics g = getComponent().getGraphics();
    drawHilight(g);
    g.dispose();
  }
  
  private void mouseExited(MouseExitedEvent e)
  {
    mouseInside = false;
    repaint();
  }
  
  private void mousePressed(MousePressedEvent e)
  {
    if (e.isPopupTrigger())
    {
      popup.show(e);
      clickPoint = null;
      return;
    }
    Graphics g = getComponent().getGraphics();
    clickPoint = e.getPoint();
    Renderer rend = ModellingApp.getPreferences().getTexturePreviewRenderer();
    if (rend != null)
      rend.cancelRendering(theScene);
    rotMatrix = Mat4.identity();
    drawObject(g);
    g.dispose();
  }

  private void mouseReleased(MouseReleasedEvent e)
  {
    if (e.isPopupTrigger())
    {
      popup.show(e);
      return;
    }
    if (clickPoint == null)
      return;
    Point dragPoint = e.getPoint();
    Vec3 rotAxis = new Vec3((clickPoint.y-dragPoint.y)*DRAG_SCALE, (dragPoint.x-clickPoint.x)*DRAG_SCALE, 0.0);
    double angle = rotAxis.length();
    if (angle != 0.0)
      {
        	rotAxis = rotAxis.times(1.0/angle);
        	rotAxis = theCamera.getViewToWorld().timesDirection(rotAxis);
        	rotMatrix = Mat4.axisRotation(rotAxis, angle);
        	objectCoords.transformAxes(rotMatrix);
      }
    render();
  }

  private void mouseClicked(MouseClickedEvent e)
  {
    if (e.getClickCount() == 2)
      {
        	objectCoords.setOrientation(0.0, 0.0, 0.0);
        	render();
      }
  }

  private void mouseDragged(MouseDraggedEvent e)
  {
    if (clickPoint == null)
      return;
    Graphics g = getComponent().getGraphics();
    Point dragPoint = e.getPoint();
    Vec3 rotAxis = new Vec3((clickPoint.y-dragPoint.y)*DRAG_SCALE, (dragPoint.x-clickPoint.x)*DRAG_SCALE, 0.0);
    double angle = rotAxis.length();
    rotAxis = rotAxis.times(1.0/angle);
    rotAxis = theCamera.getViewToWorld().timesDirection(rotAxis);
    rotMatrix = Mat4.axisRotation(rotAxis, angle);
    g.drawImage(theImage, 0, 0, getComponent());
    drawHilight(g);
    drawObject(g);
    g.dispose();
  }
}
